mod leetcode;
mod gitee;

use std::fs::File;
use std::io::{Write};
use std::ops::Add;
use std::os::unix::fs::PermissionsExt;
use std::path::PathBuf;
use clap::{Args, Parser, Subcommand};
use anyhow::{Error, Result};
use regex::Regex;
use reqwest::{Client};
use serde_json::{json, Value};
use tokio::fs;
use crate::gitee::entity::LatestRelease;
use crate::leetcode::entity::{DiscussTopicRequest, ProblemSetQuestionList, ProblemSetQuestionListRequest, Question, QuestionSolutionArticles, RequestFilter, SolutionArticle, TopicsListQueryRequest, TranslationQuestion, TranslationRequest};

const LEET_CODE_URL: &str = "https://leetcode.cn/graphql/";

const LEET_PROBLEM_PREFIX: &str = "https://leetcode-cn.com/problems/";
const LEET_PROBLEM_SUFFIX: &str = "/description/";

const LATEST_RELEASE_URL: &str = "https://gitee.com/api/v5/repos/WwWxQxQ/lcsc/releases/latest";

#[tokio::main]
async fn main() {
    if let Err(e) = run().await {
        eprintln!("An error occurred: {}", e);
        std::process::exit(1);
    }
}

#[derive(Parser)]
#[command(version, about, long_about = None)]
struct Cli {
    #[command(subcommand)]
    command: Commands,
}

#[derive(Subcommand)]
enum Commands {
    Get(ProblemArgs),
    Update,
}

#[derive(Args, Clone)]
struct ProblemArgs {
    #[arg(
        short,
        long,
        value_name = "PROBLEM",
        help = "the full name of the problem, like 1.two-sum"
    )]
    problem: String,
    #[arg(
        short,
        long,
        value_name = "FILE",
        default_value = "./",
        help = "directory path of the file to write"
    )]
    file: Option<PathBuf>,
    #[arg(
        short,
        long,
        value_name = "AUTHOR",
        help = "specify the author of the solution, default is the first solution"
    )]
    author: Option<String>,
}


async fn run() -> Result<()> {
    let cli: Cli = Cli::parse();
    match &cli.command {
        Commands::Get(args) => { get_from_leetcode((*args).clone()).await }
        Commands::Update => { update().await }
    }
}


async fn get_from_leetcode(cli: ProblemArgs) -> Result<()> {
    let problem: String = cli.problem;
    let pro_arr: Vec<&str> = problem.split('.').map(|s: &str| s.trim()).collect();
    if pro_arr.len() != 2 {
        return Err(Error::msg("Problem format is not correct!"));
    }
    let id: &str = pro_arr[0];
    let problem: String = pro_arr.join(".");
    let question: Question = get_question(problem.clone(), id).await?;
    let title_slug: String = question.get_title_slug();
    let slug: String = get_slug(title_slug.clone(), cli.author).await?;
    let solution: String = get_solution(slug).await?;
    let content: String = get_content(title_slug.clone()).await?;

    let full_content = question.get_difficulty()
        .add("\n")
        .add(&content)
        .add(&build_url(title_slug, problem.clone()))
        .add("\n\n---\n\n")
        .add(&solution);

    write_to_file(&full_content, cli.file.unwrap(), problem)
}


fn build_url(title: String, chinese: String) -> String {
    // let url = LEET_PROBLEM_PREFIX.to_string().add(&title).add(LEET_PROBLEM_SUFFIX);
    format!("[{chinese}]({LEET_PROBLEM_PREFIX}{title}{LEET_PROBLEM_SUFFIX})\n")
}

async fn get_content(title_slug: String) -> Result<String> {
    // let mut content = "<p>厨房里总共有 <code>n</code>&nbsp;个橘子，你决定每一天选择如下方式之一吃这些橘子：</p>\n\n<ul>\n\t<li>吃掉一个橘子。</li>\n\t<li>如果剩余橘子数 <code>n</code>&nbsp;能被 2 整除，那么你可以吃掉 <code>n/2</code> 个橘子。</li>\n\t<li>如果剩余橘子数&nbsp;<code>n</code>&nbsp;能被 3 整除，那么你可以吃掉 <code>2*(n/3)</code> 个橘子。</li>\n</ul>\n\n<p>每天你只能从以上 3 种方案中选择一种方案。</p>\n\n<p>请你返回吃掉所有 <code>n</code>&nbsp;个橘子的最少天数。</p>\n\n<p>&nbsp;</p>\n\n<p><strong>示例 1：</strong></p>\n\n<pre><strong>输入：</strong>n = 10\n<strong>输出：</strong>4\n<strong>解释：</strong>你总共有 10 个橘子。\n第 1 天：吃 1 个橘子，剩余橘子数 10 - 1 = 9。\n第 2 天：吃 6 个橘子，剩余橘子数 9 - 2*(9/3) = 9 - 6 = 3。（9 可以被 3 整除）\n第 3 天：吃 2 个橘子，剩余橘子数 3 - 2*(3/3) = 3 - 2 = 1。\n第 4 天：吃掉最后 1 个橘子，剩余橘子数 1 - 1 = 0。\n你需要至少 4 天吃掉 10 个橘子。\n</pre>\n\n<p><strong>示例 2：</strong></p>\n\n<pre><strong>输入：</strong>n = 6\n<strong>输出：</strong>3\n<strong>解释：</strong>你总共有 6 个橘子。\n第 1 天：吃 3 个橘子，剩余橘子数 6 - 6/2 = 6 - 3 = 3。（6 可以被 2 整除）\n第 2 天：吃 2 个橘子，剩余橘子数 3 - 2*(3/3) = 3 - 2 = 1。（3 可以被 3 整除）\n第 3 天：吃掉剩余 1 个橘子，剩余橘子数 1 - 1 = 0。\n你至少需要 3 天吃掉 6 个橘子。\n</pre>\n\n<p><strong>示例 3：</strong></p>\n\n<pre><strong>输入：</strong>n = 1\n<strong>输出：</strong>1\n</pre>\n\n<p><strong>示例 4：</strong></p>\n\n<pre><strong>输入：</strong>n = 56\n<strong>输出：</strong>6\n</pre>\n\n<p>&nbsp;</p>\n\n<p><strong>提示：</strong></p>\n\n<ul>\n\t<li><code>1 &lt;= n &lt;= 2*10^9</code></li>\n</ul>\n".to_string();
    let query_string = "\n    query questionTranslations($titleSlug: String!) {\n  question(titleSlug: $titleSlug) {\n    translatedTitle\n    translatedContent\n  }\n}\n    ";
    let request = TranslationRequest::new(title_slug);
    let res = request_leetcode(query_string, request).await?;
    let question = &res["question"].to_string();

    let question: TranslationQuestion = match serde_json::from_str(question) {
        Ok(question) => { question }
        Err(e) => { return Err(Error::msg(e.to_string())); }
    };

    let mut content = question.get_translated_content();

    let replace_pair = vec![
        (Regex::new(r"<p>").unwrap(), ""),
        (Regex::new(r"<code>").unwrap(), "`"),
        (Regex::new(r"</code>").unwrap(), "`"),
        (Regex::new(r"</p>").unwrap(), "\n"),
        (Regex::new(r"&nbsp;").unwrap(), " "),
        (Regex::new(r"<ul>").unwrap(), ""),
        (Regex::new(r"</ul>").unwrap(), ""),
        (Regex::new(r"<pre>").unwrap(), "\n```shell \n"),
        (Regex::new(r"</pre>").unwrap(), "\n``` \n"),
        (Regex::new(r"<li>").unwrap(), "- "),
        (Regex::new(r"</li>").unwrap(), ""),
        (Regex::new(r"<strong>").unwrap(), ""),
        (Regex::new(r"</strong>").unwrap(), ""),
        (Regex::new(r"&lt;=").unwrap(), "<="),
        (Regex::new(r"&lt;").unwrap(), "<"),
        (Regex::new(r"&gt;=").unwrap(), ">="),
        (Regex::new(r"&gt;").unwrap(), ">"),
        (Regex::new("<meta charset=\"UTF-8\" />").unwrap(), ""),
        (Regex::new(r"\t").unwrap(), ""),
        (Regex::new(r"<em>").unwrap(), "*"),
        (Regex::new(r"</em>").unwrap(), "*"),
        (Regex::new(r"<b>").unwrap(), ""),
        (Regex::new(r"</b>").unwrap(), ""),
        (Regex::new(r"<sub>").unwrap(), "_"),
        (Regex::new(r"</sub>").unwrap(), ""),
        (Regex::new(r"&quot;").unwrap(), "\""),
        (Regex::new(r"<ol>").unwrap(), ""),
        (Regex::new(r"</ol>").unwrap(), ""),
        (Regex::new(r"<sup>").unwrap(), "^"),
        (Regex::new(r"</sup>").unwrap(), ""),
        (Regex::new("<strong class=\"example\">").unwrap(), ""),
        (Regex::new("<div class=\"example-block\">").unwrap(), ""),
        (Regex::new(r"<br/>").unwrap(), "\n"),
        (Regex::new(r"<br />").unwrap(), "\n"),
        (Regex::new(r"<br>").unwrap(), ""),
        (Regex::new(r"<div>").unwrap(), ""),
        (Regex::new(r"</div>").unwrap(), ""),
        (Regex::new("<span class=\"example-io\">").unwrap(), ""),
        (Regex::new(r"</span>").unwrap(), ""),
        
        
    ];

    for (regex, str) in replace_pair {
        content = regex.replace_all(content.as_str(), str).to_string();
    }

    println!("==== get content size: {}", content.bytes().count());
    Ok(content)
}

fn write_to_file(content: &String, mut path: PathBuf, problem: String) -> Result<()> {
    path.push(problem.add(".md"));
    let mut file = match path.exists() {
        true => {
            match File::options().append(true).open(path) {
                Ok(file) => { file }
                Err(e) => { return Err(Error::msg(e.to_string())); }
            }
        }
        false => {
            match File::create_new(path) {
                Ok(file) => { file }
                Err(e) => { return Err(Error::msg(e.to_string())); }
            }
        }
    };

    match file.write_all(content.as_bytes()) {
        Ok(_) => {
            println!("==== file write success");
            Ok(())
        }
        Err(e) => { Err(Error::msg(e.to_string())) }
    }
}

async fn get_solution(slug: String) -> Result<String> {
    let discuss: SolutionArticle = get_discuss_topic(slug).await?;
    let content = discuss.get_content();

    println!("==== solution size: {:?}", content.bytes().count());
    Ok(content.clone())
}

async fn get_discuss_topic(slug: String) -> Result<SolutionArticle> {
    let query_string = "\n    query discussTopic($slug: String) {\n  solutionArticle(slug: $slug, orderBy: DEFAULT) {\n    ...solutionArticle\n    content\n    next {\n      slug\n      title\n    }\n    prev {\n      slug\n      title\n    }\n  }\n}\n    \n    fragment solutionArticle on SolutionArticleNode {\n  ipRegion\n  rewardEnabled\n  canEditReward\n  uuid\n  title\n  slug\n  sunk\n  chargeType\n  status\n  identifier\n  canEdit\n  canSee\n  reactionType\n  reactionsV2 {\n    count\n    reactionType\n  }\n  tags {\n    name\n    nameTranslated\n    slug\n    tagType\n  }\n  createdAt\n  thumbnail\n  author {\n    username\n    isDiscussAdmin\n    isDiscussStaff\n    profile {\n      userAvatar\n      userSlug\n      realName\n      reputation\n    }\n  }\n  summary\n  topic {\n    id\n    subscribed\n    commentCount\n    viewCount\n    post {\n      id\n      status\n      voteStatus\n      isOwnPost\n    }\n  }\n  byLeetcode\n  isMyFavorite\n  isMostPopular\n  favoriteCount\n  isEditorsPick\n  hitCount\n  videosInfo {\n    videoId\n    coverUrl\n    duration\n  }\n}\n    ";
    let request = DiscussTopicRequest::new(slug);

    let res = request_leetcode(query_string, request).await?;
    let str = &res["solutionArticle"].to_string();
    match serde_json::from_str(str) {
        Ok(discuss) => { Ok(discuss) }
        Err(e) => { Err(Error::msg(e.to_string())) }
    }
}

async fn get_question(problem: String, id: &str) -> Result<Question> {
    let problem_list: ProblemSetQuestionList = query_problem_list(problem.clone()).await?;
    // println!("==== problem_list {:?}", problem_list);
    let question = problem_list.get_questions();

    let first_question = question.first().unwrap();
    let frontend_question_id = &first_question.get_frontend_question_id();
    println!("==== frontend_question_id {:?}, id: {id}", frontend_question_id);
    if frontend_question_id.is_empty() || frontend_question_id != id {
        return Err(Error::msg("题号不一致"));
    };

    let title_slug = first_question.get_title_slug();
    println!("==== title_slug {:?}", title_slug);

    Ok(first_question.clone())
}

async fn get_slug(title_slug: String, author: Option<String>) -> Result<String> {
    let topics: QuestionSolutionArticles = query_topics(title_slug).await?;
    match author {
        None => { get_official_slug(topics).await }
        Some(author) => { get_slug_by_author(topics, author).await }
    }
}

async fn get_slug_by_author(topics: QuestionSolutionArticles, name: String) -> Result<String> {
    for edge in topics.get_edges() {
        let node = edge.get_node();
        let author = node.get_author();
        if name.eq(&author.get_username()) {
            println!("==== find author {:?}", name);
            return Ok(node.get_slug());
        }
    }

    return Err(Error::msg(format!("未找到 {} 的题解", name)));
}

async fn get_official_slug(topics: QuestionSolutionArticles) -> Result<String> {
    let edges = topics.get_edges();
    let first_edge = edges.first().unwrap();
    let node = first_edge.get_node();

    let slug = node.get_slug();
    println!("==== slug {:?} is by leetcode:{:?}", slug, node.is_by_leetcode());

    Ok(slug)
}

async fn query_topics(title_slug: String) -> Result<QuestionSolutionArticles> {
    let query_string = "\n    query questionTopicsList($questionSlug: String!, $skip: Int, $first: Int, $orderBy: SolutionArticleOrderBy, $userInput: String, $tagSlugs: [String!]) {\n  questionSolutionArticles(\n    questionSlug: $questionSlug\n    skip: $skip\n    first: $first\n    orderBy: $orderBy\n    userInput: $userInput\n    tagSlugs: $tagSlugs\n  ) {\n    totalNum\n    edges {\n      node {\n        ipRegion\n        rewardEnabled\n        canEditReward\n        uuid\n        title\n        slug\n        sunk\n        chargeType\n        status\n        identifier\n        canEdit\n        canSee\n        reactionType\n        hasVideo\n        favoriteCount\n        upvoteCount\n        reactionsV2 {\n          count\n          reactionType\n        }\n        tags {\n          name\n          nameTranslated\n          slug\n          tagType\n        }\n        createdAt\n        thumbnail\n        author {\n          username\n          profile {\n            userAvatar\n            userSlug\n            realName\n            reputation\n          }\n        }\n        summary\n        topic {\n          id\n          commentCount\n          viewCount\n          pinned\n        }\n        byLeetcode\n        isMyFavorite\n        isMostPopular\n        isEditorsPick\n        hitCount\n        videosInfo {\n          videoId\n          coverUrl\n          duration\n        }\n      }\n    }\n  }\n}\n    ";
    let request = TopicsListQueryRequest::new(
        title_slug, 0, 15, "DEFAULT".to_string(), "".to_string(), "".to_string(),
    );
    let res = request_leetcode(query_string, request).await?;
    let str = &res["questionSolutionArticles"].to_string();

    match serde_json::from_str(str) {
        Ok(slug) => { Ok(slug) }
        Err(e) => { Err(Error::msg(e.to_string())) }
    }
}


async fn query_problem_list(problem: String) -> Result<ProblemSetQuestionList> {
    let query_string = "\n    query problemsetQuestionList($categorySlug: String, $limit: Int, $skip: Int, $filters: QuestionListFilterInput) {\n  problemsetQuestionList(\n    categorySlug: $categorySlug\n    limit: $limit\n    skip: $skip\n    filters: $filters\n  ) {\n    hasMore\n    total\n    questions {\n      acRate\n      difficulty\n      freqBar\n      frontendQuestionId\n      isFavor\n      paidOnly\n      solutionNum\n      status\n      title\n      titleCn\n      titleSlug\n      topicTags {\n        name\n        nameTranslated\n        id\n        slug\n      }\n      extra {\n        hasVideoSolution\n        topCompanyTags {\n          imgUrl\n          slug\n          numSubscribed\n        }\n      }\n    }\n  }\n}\n    ";
    let request: ProblemSetQuestionListRequest = ProblemSetQuestionListRequest::new(
        "".to_string(), 0, 50,
        RequestFilter::new(
            problem
        ),
    );

    let res = request_leetcode(query_string, request).await?;
    let str = &res["problemsetQuestionList"].to_string();

    match serde_json::from_str(str) {
        Ok(problem_list) => { Ok(problem_list) }
        Err(e) => { Err(Error::msg(e.to_string())) }
    }
}

async fn update() -> Result<()> {
    let cur_version = env!("CARGO_PKG_VERSION");
    println!("current version: {cur_version}");
    let client = Client::new();
    let res = client.get(LATEST_RELEASE_URL).send().await;

    let latest_release: LatestRelease = match res {
        Ok(res) => {
            let text = res.text().await.unwrap();
            let res_json: serde_json::Value = serde_json::from_str(&text).unwrap();
            serde_json::from_value(res_json).unwrap()
        }
        Err(e) => {
            eprintln!("An error occurred: {}", e);
            return Err(Error::msg(e.to_string()));
        }
    };

    if latest_release.get_tag_name().eq(cur_version) {
        println!("Newest version {}, no newer version available", latest_release.get_tag_name());
        return Ok(());
    }

    let assets = latest_release.get_assets();
    // println!("assets: {:?}", assets);
    for asset in assets {
        let name = asset.get_name();
        if name != "lcsc" {
            continue;
        }
        let url = asset.get_browser_download_url();
        println!("Updating");

        match download(url).await {
            Ok(_) => {
                println!("Updated {}", name);
                break;
            }
            Err(e) => { return Err(Error::msg(e.to_string())); }
        }
    }
    Ok(())
}

async fn download(url: String) -> Result<()> {
    let path: PathBuf = PathBuf::from("/usr/local/bin/lcsc");
    if path.exists() {
        match fs::remove_file(&path).await {
            Ok(()) => {}
            Err(_) => { return Err(Error::msg("Permission denied, please use `sudo`")); }
        }
    }

    let client = Client::new();
    let res = match client.get(url).send().await {
        Ok(res) => { res }
        Err(err) => { return Err(Error::msg(err.to_string())); }
    };

    let content = res.bytes().await.unwrap();

    let mut file = File::create_new(&path).unwrap();
    file.write_all(&content).unwrap();

    let metadata = fs::metadata(&path).await.unwrap();
    let mut pre = metadata.permissions();
    pre.set_mode(0o755);
    fs::set_permissions(&path, pre).await.unwrap();
    Ok(())
}


async fn request_leetcode<T>(query_string: &str, request: T) -> Result<Value>
    where T: serde::Serialize
{
    let request = json!({
        "query": query_string,
        "variables": request,
    });

    let client = Client::new();
    let res = client.post(LEET_CODE_URL)
        .json(&request)
        .send()
        .await;

    match res {
        Ok(res) => {
            let text = res.text().await?;
            let res_json: serde_json::Value = serde_json::from_str(&text)?;
            let data = &res_json["data"];
            Ok(data.clone())
        }
        Err(e) => { Err(Error::msg(e.to_string())) }
    }
}
